﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Text.RegularExpressions;
using PServiceBus.Core.Runtime.Extensions;

namespace PServiceBus.Core.Runtime.Serializers {
    public static class JsonSerializer {
        private static readonly string _typeIdentifier = "___type",
            _typeAttribute = "type";

        public static string XmlToJson(string xml, bool includeDictTypeInfo = true) {
            var doc = new XmlDocument();
            doc.LoadXml(xml);
            var element = doc.ChildNodes[0];
            return ReadObject(element, IsItemList(element), includeDictTypeInfo);
        }

        private static bool IsItemList(XmlNode element) {
            var firstElement = element.ChildNodes.Count > 0 ? element.ChildNodes[0] : null;
            return firstElement != null && firstElement.Name == "Items"
                && firstElement.Attributes.Count > 0 && firstElement.Attributes["type"].InnerText == "Items";
        }

        private static string ReadObject(XmlNode element, bool isItemList = false, bool includeDictTypeInfo = false) {
            var sb = new StringBuilder();
            if (element.ChildNodes.Count > 0) {
                var count = 0;
                var isCharacterData = element.ChildNodes[0] is XmlCharacterData;
                var isJsonOrDict = !isItemList && !isCharacterData;
                if (isJsonOrDict) sb.Append("{");
                foreach (XmlNode node in element.ChildNodes) {
                    if (count > 0) sb.Append(",");
                    ReadProperty(node, sb, isItemList, includeDictTypeInfo);
                    count++;
                }
                if (isJsonOrDict) sb.Append("}\n");
            }
            else return Parse(element.InnerText, element.GetTypeStr());
            return sb.ToString();
        }

        private static string GetTypeStr(this XmlNode element) {
            return element != null && element.Attributes != null && element.Attributes[_typeAttribute] != null ? element.Attributes[_typeAttribute].Value : null;
        }

        private static string GetKeyValue(string key, string value) {
            if (key == "#text") return value;
            var startAndEndWithQuote = key.StartsWith("\"") && key.EndsWith("\"");
            return String.Format("{0}{1}{0} : {2}", !startAndEndWithQuote ? "\"" : string.Empty,  key, value);
        }

        private static readonly Regex _boolRegex = new Regex(@"^(false)$|^(true)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

        private static readonly Regex _dateRegex = new Regex(@"^\d{1,2}/\d{1,2}/\d{4}", RegexOptions.Compiled | RegexOptions.IgnoreCase);

        private static string Parse(string text, string typeStr = null) {
            var nvalue = default(decimal);
            if (text == "null") return text;
            if (_dateRegex.IsMatch(text)) {
                var date = Convert.ToDateTime(text);
                return String.Format("\"{0:yyyy-MM-ddTHH:mm:ss.fffZ}\"", date);
            }
            if (_boolRegex.IsMatch(text)) return text.ToLower();
            if ((String.IsNullOrWhiteSpace(typeStr) || (typeStr != null && typeStr != "str")) && decimal.TryParse(text, out nvalue)) return text;
            return String.Format("\"{0}\"", text.Escape().Replace(@"\",@"\\"));
        }

        private static void ReadProperty(XmlNode element, StringBuilder sb, bool isItemList = false, bool includeDictTypeInfo = false) {
            var isItems = element.Name == "Items" && element.Attributes.Count > 0 && element.Attributes[_typeAttribute].InnerText == "Items";
            var propName = element.Name;
            if (!isItems) {
                isItems = IsItemList(element);
                if (isItems) element = element.ChildNodes[0];
            }
            if (!isItems) {
                if (element.ChildNodes.Count > 0 && element.ChildNodes[0].Name != "#text") 
                    sb.Append(GetKeyValue(element.Name, ReadObject(element, isItemList: false, includeDictTypeInfo: includeDictTypeInfo)));
                else
                    sb.Append(GetKeyValue(element.Name, Parse(element.InnerText, element.GetTypeStr())));
            } else ReadItemsProperty(propName, element, sb, isItemList, includeDictTypeInfo);
        }

        private static void ReadItemsProperty(string name, XmlNode element, StringBuilder sb, bool isItemList = false, bool includeDictTypeInfo = false) {
            var items = new List<string>();
            var elementChildNodes = element.ChildNodes;
            var firstElement = elementChildNodes.Count > 0 ? elementChildNodes[0] : null;
            var isDict = firstElement != null &&
                firstElement.Attributes != null && firstElement.Attributes.Count > 0 
                && firstElement.Attributes[_typeAttribute] != null && firstElement.Attributes[_typeAttribute].Value == "dict";
            if (isDict && includeDictTypeInfo)
                items.Add(GetKeyValue(_typeIdentifier, Parse("dict")));
            
            foreach (XmlNode item in elementChildNodes) {
                if (isDict) {
                    var itemNodes = item.ChildNodes;
                    var keyItemNode = itemNodes[0];
                    var valueItemNode = itemNodes[1];
                    var key = ReadObject(keyItemNode, 
                        keyItemNode.LastChild != null && keyItemNode.LastChild.NodeType != XmlNodeType.Element, includeDictTypeInfo);
                    var value = ReadObject(valueItemNode,
                        valueItemNode.LastChild != null && valueItemNode.LastChild.NodeType != XmlNodeType.Element, includeDictTypeInfo);
                    items.Add(GetKeyValue(key, value));
                }
                else items.Add(ReadObject(item, isItemList: false, includeDictTypeInfo: includeDictTypeInfo));
            }
            var text = String.Format("{0}{1}{2}", isDict ? "{" : "[", String.Join(",", items), isDict ? "}" : "]");
            text = isItemList ? text : GetKeyValue(name, text);
            sb.Append(text);
        }
    }
}
